/*
 * heatmap.js 1.0 -    JavaScript Heatmap Library
 *
 * Copyright (c) 2011, Patrick Wied (http://www.patrick-wied.at)
 * Dual-licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
 * and the Beerware (http://en.wikipedia.org/wiki/Beerware) license.
 */

(function(w){
    // the heatmapFactory creates heatmap instances
    var heatmapFactory = (function(){

        // store object constructor
        // a heatmap contains a store
        // the store has to know about the heatmap in order to trigger heatmap updates when datapoints get added
        var store = function store(hmap){

            var _ = {
                // data is a two dimensional array
                // a datapoint gets saved as data[point-x-value][point-y-value]
                // the value at [point-x-value][point-y-value] is the occurrence of the datapoint
                data: [],
                // tight coupling of the heatmap object
                heatmap: hmap
            };
            // the max occurrence - the heatmaps radial gradient alpha transition is based on it
            this.max = 1;

            this.get = function(key){
                return _[key];
            };
            this.set = function(key, value){
                _[key] = value;
            };
        }

        store.prototype = {
            // function for adding datapoints to the store
            // datapoints are usually defined by x and y but could also contain a third parameter which represents the occurrence
            addDataPoint: function(x, y){
                if(x < 0 || y < 0)
                    return;

                var me = this,
                    heatmap = me.get("heatmap"),
                    data = me.get("data");

                if(!data[x])
                    data[x] = [];

                if(!data[x][y])
                    data[x][y] = 0;

                // if count parameter is set increment by count otherwise by 1
                data[x][y]+=(arguments.length<3)?1:arguments[2];

                me.set("data", data);
                // do we have a new maximum?
                if(me.max < data[x][y]){
                    // max changed, we need to redraw all existing(lower) datapoints
                    heatmap.get("actx").clearRect(0,0,heatmap.get("width"),heatmap.get("height"));
                    me.setDataSet({ max: data[x][y], data: data }, true);
                    return;
                }
                heatmap.drawAlpha(x, y, data[x][y], true);
            },
            setDataSet: function(obj, internal){
                var me = this,
                    heatmap = me.get("heatmap"),
                    data = [],
                    d = obj.data,
                    dlen = d.length;
                // clear the heatmap before the data set gets drawn
                heatmap.clear();
                this.max = obj.max;
                // if a legend is set, update it
                heatmap.get("legend") && heatmap.get("legend").update(obj.max);

                if(internal != null && internal){
                    for(var one in d){
                        // jump over undefined indexes
                        if(one === undefined)
                            continue;
                        for(var two in d[one]){
                            if(two === undefined)
                                continue;
                            // if both indexes are defined, push the values into the array
                            heatmap.drawAlpha(one, two, d[one][two], false);
                        }
                    }
                }else{
                    while(dlen--){
                        var point = d[dlen];
                        heatmap.drawAlpha(point.x, point.y, point.count, false);
                        if(!data[point.x])
                            data[point.x] = [];

                        if(!data[point.x][point.y])
                            data[point.x][point.y] = 0;

                        data[point.x][point.y] = point.count;
                    }
                }
                heatmap.colorize();
                this.set("data", d);
            },
            exportDataSet: function(){
                var me = this,
                    data = me.get("data"),
                    exportData = [];

                for(var one in data){
                    // jump over undefined indexes
                    if(one === undefined)
                        continue;
                    for(var two in data[one]){
                        if(two === undefined)
                            continue;
                        // if both indexes are defined, push the values into the array
                        exportData.push({x: parseInt(one, 10), y: parseInt(two, 10), count: data[one][two]});
                    }
                }

                return { max: me.max, data: exportData };
            },
            generateRandomDataSet: function(points){
                var heatmap = this.get("heatmap"),
                    w = heatmap.get("width"),
                    h = heatmap.get("height");
                var randomset = {},
                    max = Math.floor(Math.random()*1000+1);
                randomset.max = max;
                var data = [];
                while(points--){
                    data.push({x: Math.floor(Math.random()*w+1), y: Math.floor(Math.random()*h+1), count: Math.floor(Math.random()*max+1)});
                }
                randomset.data = data;
                this.setDataSet(randomset);
            }
        };

        var legend = function legend(config){
            this.config = config;

            var _ = {
                element: null,
                labelsEl: null,
                gradientCfg: null,
                ctx: null
            };
            this.get = function(key){
                return _[key];
            };
            this.set = function(key, value){
                _[key] = value;
            };
            this.init();
        };
        legend.prototype = {
            init: function(){
                var me = this,
                    config = me.config,
                    title = config.title || "Legend",
                    position = config.position,
                    offset = config.offset || 10,
                    gconfig = config.gradient,
                    labelsEl = document.createElement("ul"),
                    labelsHtml = "",
                    grad, element, gradient, positionCss = "";

                me.processGradientObject();

                // Positioning

                // top or bottom
                if(position.indexOf('t') > -1){
                    positionCss += 'top:'+offset+'px;';
                }else{
                    positionCss += 'bottom:'+offset+'px;';
                }

                // left or right
                if(position.indexOf('l') > -1){
                    positionCss += 'left:'+offset+'px;';
                }else{
                    positionCss += 'right:'+offset+'px;';
                }

                element = document.createElement("div");
                element.style.cssText = "border-radius:5px;position:absolute;"+positionCss+"font-family:Helvetica; width:256px;z-index:10000000000; background:rgba(255,255,255,1);padding:10px;border:1px solid black;margin:0;";
                element.innerHTML = "<h3 style='padding:0;margin:0;text-align:center;font-size:16px;'>"+title+"</h3>";
                // create gradient in canvas
                labelsEl.style.cssText = "position:relative;font-size:12px;display:block;list-style:none;list-style-type:none;margin:0;height:15px;";


                // create gradient element
                gradient = document.createElement("div");
                gradient.style.cssText = ["position:relative;display:block;width:256px;height:15px;border-bottom:1px solid black; background-image:url(",me.createGradientImage(),");"].join("");

                element.appendChild(labelsEl);
                element.appendChild(gradient);

                me.set("element", element);
                me.set("labelsEl", labelsEl);

                me.update(1);
            },
            processGradientObject: function(){
                // create array and sort it
                var me = this,
                    gradientConfig = this.config.gradient,
                    gradientArr = [];

                for(var key in gradientConfig){
                    if(gradientConfig.hasOwnProperty(key)){
                        gradientArr.push({ stop: key, value: gradientConfig[key] });
                    }
                }
                gradientArr.sort(function(a, b){
                    return (a.stop - b.stop);
                });
                gradientArr.unshift({ stop: 0, value: 'rgba(0,0,0,0)' });

                me.set("gradientArr", gradientArr);
            },
            createGradientImage: function(){
                var me = this,
                    gradArr = me.get("gradientArr"),
                    length = gradArr.length,
                    canvas = document.createElement("canvas"),
                    ctx = canvas.getContext("2d"),
                    grad;
                // the gradient in the legend including the ticks will be 256x15px
                canvas.width = "256";
                canvas.height = "15";

                grad = ctx.createLinearGradient(0,5,256,10);

                for(var i = 0; i < length; i++){
                    grad.addColorStop(1/(length-1) * i, gradArr[i].value);
                }

                ctx.fillStyle = grad;
                ctx.fillRect(0,5,256,10);
                ctx.strokeStyle = "black";
                ctx.beginPath();

                for(var i = 0; i < length; i++){
                    ctx.moveTo(((1/(length-1)*i*256) >> 0)+.5, 0);
                    ctx.lineTo(((1/(length-1)*i*256) >> 0)+.5, (i==0)?15:5);
                }
                ctx.moveTo(255.5, 0);
                ctx.lineTo(255.5, 15);
                ctx.moveTo(255.5, 4.5);
                ctx.lineTo(0, 4.5);

                ctx.stroke();

                // we re-use the context for measuring the legends label widths
                me.set("ctx", ctx);

                return canvas.toDataURL();
            },
            getElement: function(){
                return this.get("element");
            },
            update: function(max){
                var me = this,
                    gradient = me.get("gradientArr"),
                    ctx = me.get("ctx"),
                    labels = me.get("labelsEl"),
                    labelText, labelsHtml = "", offset;

                for(var i = 0; i < gradient.length; i++){

                    labelText = max*gradient[i].stop >> 0;
                    offset = (ctx.measureText(labelText).width/2) >> 0;

                    if(i == 0){
                        offset = 0;
                    }
                    if(i == gradient.length-1){
                        offset *= 2;
                    }
                    labelsHtml += '<li style="position:absolute;left:'+(((((1/(gradient.length-1)*i*256) || 0)) >> 0)-offset+.5)+'px">'+labelText+'</li>';
                }
                labels.innerHTML = labelsHtml;
            }
        };

        // heatmap object constructor
        var heatmap = function heatmap(config){
            // private variables
            var _ = {
                radius : 40,
                element : {},
                canvas : {},
                acanvas: {},
                ctx : {},
                actx : {},
                legend: null,
                visible : true,
                width : 0,
                height : 0,
                max : false,
                gradient : false,
                opacity: 180,
                premultiplyAlpha: false,
                bounds: {
                    l: 1000,
                    r: 0,
                    t: 1000,
                    b: 0
                },
                debug: false
            };
            // heatmap store containing the datapoints and information about the maximum
            // accessible via instance.store
            this.store = new store(this);

            this.get = function(key){
                return _[key];
            };
            this.set = function(key, value){
                _[key] = value;
            };
            // configure the heatmap when an instance gets created
            this.configure(config);
            // and initialize it
            this.init();
        };

        // public functions
        heatmap.prototype = {
            configure: function(config){
                var me = this,
                    rout, rin;

                me.set("radius", config["radius"] || 40);
                me.set("element", (config.element instanceof Object)?config.element:document.getElementById(config.element));
                me.set("visible", (config.visible != null)?config.visible:true);
                me.set("max", config.max || false);
                me.set("gradient", config.gradient || { 0.45: "rgb(0,0,255)", 0.55: "rgb(0,255,255)", 0.65: "rgb(0,255,0)", 0.95: "yellow", 1.0: "rgb(255,0,0)"});    // default is the common blue to red gradient
                me.set("opacity", parseInt(255/(100/config.opacity), 10) || 180);
                me.set("width", config.width || 0);
                me.set("height", config.height || 0);
                me.set("debug", config.debug);

                if(config.legend){
                    var legendCfg = config.legend;
                    legendCfg.gradient = me.get("gradient");
                    me.set("legend", new legend(legendCfg));
                }

            },
            resize: function () {
                var me = this,
                    element = me.get("element"),
                    canvas = me.get("canvas"),
                    acanvas = me.get("acanvas");
                canvas.width = acanvas.width = me.get("width") || element.style.width.replace(/px/, "") || me.getWidth(element);
                this.set("width", canvas.width);
                canvas.height = acanvas.height = me.get("height") || element.style.height.replace(/px/, "") || me.getHeight(element);
                this.set("height", canvas.height);
            },

            init: function(){
                var me = this,
                    canvas = document.createElement("canvas"),
                    acanvas = document.createElement("canvas"),
                    ctx = canvas.getContext("2d"),
                    actx = acanvas.getContext("2d"),
                    element = me.get("element");


                me.initColorPalette();

                me.set("canvas", canvas);
                me.set("ctx", ctx);
                me.set("acanvas", acanvas);
                me.set("actx", actx);

                me.resize();
                canvas.style.cssText = acanvas.style.cssText = "position:absolute;top:60px;margin-left:0px;z-index:10000000;";

                if(!me.get("visible"))
                    canvas.style.display = "none";

                element.appendChild(canvas);
                if(me.get("legend")){
                    element.appendChild(me.get("legend").getElement());
                }

                // debugging purposes only
                if(me.get("debug"))
                    document.body.appendChild(acanvas);

                actx.shadowOffsetX = 15000;
                actx.shadowOffsetY = 15000;
                actx.shadowBlur = 15;
            },
            initColorPalette: function(){

                var me = this,
                    canvas = document.createElement("canvas"),
                    gradient = me.get("gradient"),
                    ctx, grad, testData;

                canvas.width = "1";
                canvas.height = "256";
                ctx = canvas.getContext("2d");
                grad = ctx.createLinearGradient(0,0,1,256);

                // Test how the browser renders alpha by setting a partially transparent pixel
                // and reading the result.  A good browser will return a value reasonably close
                // to what was set.  Some browsers (e.g. on Android) will return a ridiculously wrong value.
                testData = ctx.getImageData(0,0,1,1);
                testData.data[0] = testData.data[3] = 64; // 25% red & alpha
                testData.data[1] = testData.data[2] = 0; // 0% blue & green
                ctx.putImageData(testData, 0, 0);
                testData = ctx.getImageData(0,0,1,1);
                me.set("premultiplyAlpha", (testData.data[0] < 60 || testData.data[0] > 70));

                for(var x in gradient){
                    grad.addColorStop(x, gradient[x]);
                }

                ctx.fillStyle = grad;
                ctx.fillRect(0,0,1,256);

                me.set("gradient", ctx.getImageData(0,0,1,256).data);
            },
            getWidth: function(element){
                var width = element.offsetWidth;
                if(element.style.paddingLeft){
                    width+=element.style.paddingLeft;
                }
                if(element.style.paddingRight){
                    width+=element.style.paddingRight;
                }

                return width;
            },
            getHeight: function(element){
                var height = element.offsetHeight;
                if(element.style.paddingTop){
                    height+=element.style.paddingTop;
                }
                if(element.style.paddingBottom){
                    height+=element.style.paddingBottom;
                }

                return height;
            },
            colorize: function(x, y){
                // get the private variables
                var me = this,
                    width = me.get("width"),
                    radius = me.get("radius"),
                    height = me.get("height"),
                    actx = me.get("actx"),
                    ctx = me.get("ctx"),
                    x2 = radius * 3,
                    premultiplyAlpha = me.get("premultiplyAlpha"),
                    palette = me.get("gradient"),
                    opacity = me.get("opacity"),
                    bounds = me.get("bounds"),
                    left, top, bottom, right,
                    image, imageData, length, alpha, offset, finalAlpha;

                if(x != null && y != null){
                    if(x+x2>width){
                        x=width-x2;
                    }
                    if(x<0){
                        x=0;
                    }
                    if(y<0){
                        y=0;
                    }
                    if(y+x2>height){
                        y=height-x2;
                    }
                    left = x;
                    top = y;
                    right = x + x2;
                    bottom = y + x2;

                }else{
                    if(bounds['l'] <= 1){
                        left = 1;
                    }else{
                        left = bounds['l'];
                    }
                    if(bounds['r'] > width){
                        right = width;
                    }else{
                        right = bounds['r'];
                    }
                    if(bounds['t'] <= 1){
                        top = 1;
                    }else{
                        top = bounds['t'];
                    }
                    if(bounds['b'] > height){
                        bottom = height;
                    }else{
                        bottom = bounds['b'];
                    }
                }

                image = actx.getImageData(left, top, right-left, bottom-top);
                imageData = image.data;
                length = imageData.length;
                // loop thru the area
                for(var i=3; i < length; i+=4){

                    // [0] -> r, [1] -> g, [2] -> b, [3] -> alpha
                    alpha = imageData[i],
                        offset = alpha*4;

                    if(!offset)
                        continue;

                    // we ve started with i=3
                    // set the new r, g and b values
                    finalAlpha = (alpha < opacity)?alpha:opacity;
                    imageData[i-3]=palette[offset];
                    imageData[i-2]=palette[offset+1];
                    imageData[i-1]=palette[offset+2];

                    if (premultiplyAlpha) {
                        // To fix browsers that premultiply incorrectly, we'll pass in a value scaled
                        // appropriately so when the multiplication happens the correct value will result.
                        imageData[i-3] /= 255/finalAlpha;
                        imageData[i-2] /= 255/finalAlpha;
                        imageData[i-1] /= 255/finalAlpha;
                    }

                    // we want the heatmap to have a gradient from transparent to the colors
                    // as long as alpha is lower than the defined opacity (maximum), we'll use the alpha value
                    imageData[i] = finalAlpha;
                }
                // the rgb data manipulation didn't affect the ImageData object(defined on the top)
                // after the manipulation process we have to set the manipulated data to the ImageData object
                image.data = imageData;
                ctx.putImageData(image, left, top);
            },
            drawAlpha: function(x, y, count, colorize){
                // storing the variables because they will be often used
                var me = this,
                    radius = me.get("radius"),
                    ctx = me.get("actx"),
                    max = me.get("max"),
                    bounds = me.get("bounds"),
                    xb = x - (1.5 * radius) >> 0, yb = y - (1.5 * radius) >> 0,
                    xc = x + (1.5 * radius) >> 0, yc = y + (1.5 * radius) >> 0;

                ctx.shadowColor = ('rgba(0,0,0,'+((count)?(count/me.store.max):'0.1')+')');

                ctx.shadowOffsetX = 15000;
                ctx.shadowOffsetY = 15000;
                ctx.shadowBlur = 15;

                ctx.beginPath();
                ctx.arc(x - 15000, y - 15000, radius, 0, Math.PI * 2, true);
                ctx.closePath();
                ctx.fill();
                if(colorize){
                    // finally colorize the area
                    me.colorize(xb,yb);
                }else{
                    // or update the boundaries for the area that then should be colorized
                    if(xb < bounds["l"]){
                        bounds["l"] = xb;
                    }
                    if(yb < bounds["t"]){
                        bounds["t"] = yb;
                    }
                    if(xc > bounds['r']){
                        bounds['r'] = xc;
                    }
                    if(yc > bounds['b']){
                        bounds['b'] = yc;
                    }
                }
            },
            toggleDisplay: function(){
                var me = this,
                    visible = me.get("visible"),
                    canvas = me.get("canvas");

                if(!visible)
                    canvas.style.display = "block";
                else
                    canvas.style.display = "none";

                me.set("visible", !visible);
            },
            // dataURL export
            getImageData: function(){
                return this.get("canvas").toDataURL();
            },
            clear: function(){
                var me = this,
                    w = me.get("width"),
                    h = me.get("height");

                me.store.set("data",[]);
                // @TODO: reset stores max to 1
                //me.store.max = 1;
                me.get("ctx").clearRect(0,0,w,h);
                me.get("actx").clearRect(0,0,w,h);
            },
            cleanup: function(){
                var me = this;
                me.get("element").removeChild(me.get("canvas"));
            }
        };

        return {
            create: function(config){
                return new heatmap(config);
            },
            util: {
                mousePosition: function(ev){
                    // this doesn't work right
                    // rather use
                    /*
                     // this = element to observe
                     var x = ev.pageX - this.offsetLeft;
                     var y = ev.pageY - this.offsetTop;

                     */
                    var x, y;

                    if (ev.layerX) { // Firefox
                        x = ev.layerX;
                        y = ev.layerY;
                    } else if (ev.offsetX) { // Opera
                        x = ev.offsetX;
                        y = ev.offsetY;
                    }
                    if(typeof(x)=='undefined')
                        return;

                    return [x,y];
                }
            }
        };
    })();
    w.h337 = w.heatmapFactory = heatmapFactory;
})(window);